//Copyright (C) 2012 Chirila Alexandru
//This file is part of Robo-GoldMiner
//
//Robo-GoldMiner is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//
//Robo-GoldMiner is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with Robo-GoldMiner.  If not, see <http://www.gnu.org/licenses/>.

package gold;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * The model of the game, represent a maze with a player, that can 
 * move trough the maze and has to collect all the gold coins. The 
 * player moves according to a script given.
 * @author Alexandru Chirila
 */
public class Maze implements Serializable {
	
	private static final long serialVersionUID = -241550969456224036L;

	/**
	 * Provides a backup for the current maze.  
	 */
	private static Maze backup;
	
	/**
	 * Restores the backup, therefore reseting the game to it's
	 * original place.
	 * @return the backed-up game
	 */
	public static Maze reset(){
		return backup.copy();
	}
	
	/**
	 * Maze representation <br/>
	 * 
	 * <pre>
	 * 		Internal  External
	 * 		-2 hole  	*
	 * 		-1 wall  	/
	 * 		0 start  	S
	 * 		1 clear  	0
	 * 		2 money  	$
	 * </pre>
	 */
	private int[][] maze;

	/**
	 * Represents the colors of the rooms of the maze: <br />
	 * <pre>
	 	1 - red
	 	2 - green
	 	4 - blue
	 * </pre>
	 * The colors numbers are chosen to be the same as the ones
	 * used in the ANSI escape sequences.
	 */
	private int[][] colors;

	/**
	 * The size of the maze  (height and width)
	 */
	private int size;

	/**
	 * Player current position (x * size + y)
	 */
	private int player;

	/**
	 * Players current direction <br />
	 * 
	 * <pre>
	 * 				N				0
	 * 			W		E		3		1
	 * 				S				2
	 * </pre>
	 */
	private int direction;

	/**
	 * The observer for this model. Has the update() method
	 * to notify a change in the model.
	 */
	private View observer;

	/**
	 * The total number of gold coins in the game.
	 */
	private int moneyNo = 0;
	
	/**
	 * The number of gold coins the player has gathered so far.
	 */
	private int moneySoFar = 0;
	
	/**
	 * When this is true, the next command will be skipped and
	 * the field reseted.
	 */
	private boolean skip = false;

	/**
	 * Hash map with the external representation as key and the internal
	 * representation as the value.
	 */
	private Map<String, Integer> fileRepr = new HashMap<String, Integer>();

	public Maze(){
	}
	
	/**
	 * Returns a clone of the model.
	 */
	private Maze copy(){
		Maze clone = new Maze();
		
		clone.maze = new int[size][size];
		
		for (int i = 0; i < this.maze.length; i++) {
			for (int j = 0; j < this.maze[i].length; j++) {
				clone.maze[i][j] = maze[i][j];
			}
		}
		
		clone.colors = new int[size][size];
		
		for (int i = 0; i < this.colors.length; i++) {
			for (int j = 0; j < this.colors[i].length; j++) {
				clone.colors[i][j] = colors[i][j];
			}
		}
		
		clone.size = this.size;
		clone.player = this.player;
		clone.direction = this.direction;
		clone.observer = this.observer;
		clone.moneyNo = this.moneyNo;
		clone.moneySoFar = this.moneySoFar;
		clone.skip = this.skip;
		
		return clone;
	}
	
	/**
	 * Open the game from a file.
	 * <pre>
	 * Internal  External
	 * 	-2 hole  	*
	 * 	-1 wall  	/
	 * 	0 start  	S
	 * 	1 clear  	0
	 * 	2 money  	$
	 * 
	 * Color Code:
	 * File		External 	Internal
	 * 	1		 blue			4
	 *  2		  red			1
	 *  3		 green			2
	 *  
	 *  Example 02 - means that the room is clear and it's colored red
	 *  		$1 - means that the room is clear contains a gold coin and it's colored blue
	 *  
	 *  The walls and the holes do not receive a color
	 *  
	 *  </pre>
	 * 
	 * @param file the path of the file
	 * @param obs the observer 
	 * @throws IOException
	 * @throws ParseException
	 */
	public void open(String file, View obs) throws IOException, ParseException {
		InputStreamReader in = new InputStreamReader(new FileInputStream(file));
		BufferedReader buf = new BufferedReader(in);
		String line;
		StringTokenizer st;
		String token;

		loadMap();

		line = buf.readLine();
		size = Integer.parseInt(line);
		maze = new int[size][size];
		colors = new int[size][size];

		for (int i = 0; i < size; i++) {
			line = buf.readLine();
			st = new StringTokenizer(line);
			for (int j = 0; j < size; j++) {
				token = st.nextToken();
				String c = new String();
				c += token.charAt(0);

				if (fileRepr.containsKey(c)) {
					maze[i][j] = fileRepr.get(c);
				} else {
					throw new ParseException("Error parsing " + c
							+ " nu such symbol defined");
				}

				if (maze[i][j] >= 0) {
					if (token.length() < 2) {
						throw new ParseException("Error parsing " + c
								+ " expected color after token");
					} else {
						colors[i][j] = getColorFromFileRepr(token.charAt(1));
					}
					if (maze[i][j] == 0)
						player = i * size + j;
				}
				if (maze[i][j] == 2) {
					moneyNo++;
				}
			}
		}

		observer = obs;
		obs.update("You enter a strange maze");
		
		Maze.backup = this.copy();
		
		in.close();
	}
	
	/**
	 * Conditional command, sets to true or false the skipping of the next command	
	 * @param color the color name (red, green or blue)
	 * @return true if the command was executed
	 * @throws ParseException
	 */
	public boolean checkConditionalColor(String color) throws ParseException {
			
		if (colors[player / size][player % size] == getColorInternalRepr(color)) {
			skip = false;
			return true;
		} else {
			skip = true;
			return false;
		}
	}

	private int getColorInternalRepr(String color) throws ParseException {

		if (color.equalsIgnoreCase("red")) {
			return 1;
		}
		if (color.equalsIgnoreCase("blue")) {
			return 4;
		}
		if (color.equalsIgnoreCase("green")) {
			return 2;
		}
		throw new ParseException("No such color " + color);

	}

	private String getColorExternalRepr(int c) {
		switch (c) {
		case 1:
			return "red";
		case 2:
			return "green";
		case 4:
			return "blue";
		default:
			return "";
		}
	}

	public String getCurrentColorExternalRepr() {
		return getColorExternalRepr(colors[player / size][player % size]);
	}

	public void setCurrentColor(String cl) throws ParseException {
		colors[player / size][player % size] = getColorInternalRepr(cl);
		observer.update("%-You change the color of the room, with your incredible powers of paiting");
	}

	public int getColorFromFileRepr(char c) {
		switch (c) {
		case '1':
			return 4;
		case '2':
			return 1;
		case '3':
			return 2;
		default:
			return -1;
		}
	}

	/**
	 * Turns clockwise
	 * @return true if the command was executed
	 */
	public boolean turnCW() {
		if (skip == false) {
			direction = (direction + 1) % 4;
			observer.update("@-You turn Clockwise");
			return true;
		} else {
			skip = false;
			return false;
		}
	}

	/**
	 * Turns counter clockwise
	 * @return true if the command was executed
	 */
	public boolean turnCCW() {
		if (skip == false) {
			direction = (direction - 1) % 4;
			if (direction < 0)
				direction += 4;
			observer.update("@-You turn Counter Clockwise");
			return true;
		} else {
			skip = false;
			return false;
		}
	}

	/**
	 * Paints the room.
	 * @param cl name of the color
	 * @return true if the command was executed
	 * @throws ParseException
	 */
	public boolean changeColor(String cl) throws ParseException {
		if (skip == false) {
			setCurrentColor(cl);
			return true;
		} else {
			skip = false;
			return false;
		}
	}

	public void setPlayer(int xy) {
		player = xy;
		observer.update("");
	}

	public int getPlayer() {
		return player;
	}

	/**
	 * Tries to move the player forward. 
	 * Notice: this method will return true even if the command 
	 * was executed but no actual movement took place.
	 * @return true if the command was executed
	 * @throws EndGame If the player wins or dies
	 */
	public boolean move() throws EndGame {

		String response = null;
		int newPosX = player / size;
		int newPosY = player % size;
		int newPos;
		int place;
		if (skip == false) {
			switch (direction) {
			case 0: // North
				newPosX -= 1;
				break;
			case 1: // East
				newPosY += 1;
				break;
			case 2: // South
				newPosX += 1;
				break;
			case 3: // West
				newPosY -= 1;
				break;
			default:
				break;
			}

			if ((newPosX < size) && (newPosX >= 0) && (newPosY < size)
					&& (newPosY >= 0)) {
				place = maze[newPosX][newPosY];
				newPos = newPosX * size + newPosY;

				switch (place) {
				case -2:
					player = -1;
					throw new EndGame("|-You Lose! You have fallen in a hole");
				case -1:
					response = "?-You bash your head against the wall. The wall doesn't crack";
					break;
				case 0:
					player = newPos;
					response = "0-You move forward and find out this is where it all began";
					break;
				case 1:
					player = newPos;
					response = "~-You move forward to a new and exciting place";
					break;
				case 2:
					player = newPos;
					maze[player / size][player % size] = 1;
					response = "$-You move forward and find a shiny gold coin. Still no sign of food...";
					moneySoFar++;

					if (moneySoFar == moneyNo) {
						throw new EndGame("*-You Win! You got all the money");
					}
					break;
				}
			} else {
				response = "You try to escape through the outter wall. You fail miserably";
			}
			observer.update(response);
			return true;
		} else {
			skip = false;
			return false;
		}
	}
	
	
	public boolean isSkip() {
		return skip;
	}

	public void setSkip(boolean skip) {
		this.skip = skip;
	}

	private void loadMap() {
		fileRepr.put("*", -2); // hole
		fileRepr.put("/", -1); // wall
		fileRepr.put("S", 0); // start
		fileRepr.put("0", 1); // clear
		fileRepr.put("$", 2); // money
	}

	/**
	 * Draws the maze using ANSI escape sequences.
	 * <pre>
	 * -------
	 * |     |
	 * |  @  |	- This is a hole
	 * |     |
	 * -------
	 * 
	 * -------
	 * |     |
	 * |     |	- This is a wall (it has no background)
	 * |     |
	 * -------
	 * -------
	 * |     |
	 * |  P> |	- This is the player (the arrow is his direction)
	 * |     |
	 * -------
	 * -------
	 * |     |
	 * |     |	- This is a clear space (it has a background color)
	 * |     |
	 * -------
	 * 
	 * -------
	 * |     |
	 * |  $  |	- This is a gold coin.
	 * |     |
	 * -------
	 * </pre>
	 */
	public void draw() {

		int cl;

		for (int i = 0; i < maze.length * 3; i++) {
			for (int j = 0; j < maze[i / 3].length; j++) {

				cl = colors[i / 3][j];

				switch (i % 3) {
				case 0: // first row of drawing
					if (player == ((i / 3) * size + j)) {
						//View.escape("1m");
						if (direction == 0) {
							View.out("  ^  ", 0, cl);
						} else {
							View.out("     ", 0, cl);
						}
						//View.escape("0m");
					} else {
						switch (maze[i / 3][j]) {
						case -2:
						case -1:
							View.out("     ");
							break;
						case 0:
						case 1:
							View.out("     ", 0, cl);
							break;
						case 2:
							View.out("     ", 0, cl);
							break;
						default:
							break;
						}
					}
					break;
				case 1: // second row of drawing
					if (player == ((i / 3) * size + j)) {
						//View.escape("1m");
						if (direction == 1) {							
							View.out("  P> ", 0, cl);
						} else if (direction == 3) {
							View.out(" <P  ", 0, cl);
						} else {
							View.out("  P  ", 0, cl);
						}
						//View.escape("0m");
					} else {
						switch (maze[i / 3][j]) {
						case -2:
							View.out("  @  ");
							break;
						case -1:
							View.out("     ");
							break;
						case 0:
						case 1:
							View.out("     ", 0, cl);
							break;
						case 2:
							View.out("  $  ", 0, cl);
							break;
						default:
							break;
						}
					}
					break;
				case 2: // last row of drawing
					if (player == ((i / 3) * size + j)) {
						//View.escape("1m");
						if (direction == 2) {
							View.out("  v  ", 0, cl);
						} else {
							View.out("     ", 0, cl);
						}
						//View.escape("0m");
					} else {
						switch (maze[i / 3][j]) {
						case -2:
						case -1:
							View.out("     ");
							break;
						case 0:
						case 1:
							View.out("     ", 0, cl);
							break;
						case 2:
							View.out("     ", 0, cl);
							break;
						default:
							break;
						}
					}
					break;
				default:
					break;
				}
				if (j != size - 1) {
					View.out("|");
				}
			}
			View.out("\n");
			if (i % 3 == 2 && i != size * 3 - 1) {
				for (int k = 0; k < size * 6 - 1; k++) {
					View.out("-");
				}
				View.out("\n");
			}
		}
		View.out("Gold Coins: " + moneySoFar + "/" + moneyNo + "\n");

	}

	public void debug() {
		for (int[] row : maze) {
			System.out.print("\n");
			for (int i : row) {
				System.out.print(i + " ");
			}
		}
		System.out.println("\nPlayer: " + player / size + " " + player % size);
		System.out.println("Direction: " + direction);
		System.out.println("Money: " + moneySoFar + "/" + moneyNo);
	}
	
	private void writeObject(ObjectOutputStream stream) throws IOException {
		stream.defaultWriteObject();
	}

	private void readObject(ObjectInputStream stream) throws IOException,
			ClassNotFoundException {
		stream.defaultReadObject();
	}

}
